/*
Copyright (c) 2013 Mihail Volkov

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

#include <cassert>
#include <QtNetwork/QHostAddress>
#include "connection.h"
#include "consoleapp.h"
#include "unit.h"

#define MIN_MAX_PACKET_SIZE 24
#define MAX_MAX_PACKET_SIZE 262144 // a reasonable limit

// up to how much bytes can be pending in the buffer
// when uploading files, somewhat limits bandwidth
#define MAX_UPLD_FILE_PENDING_BYTES 4096

HtMdConnection *HtMdConnection::spInstance = NULL;

//
// commands
//

// application side:
// say hello. sent on startup, tells the client value of this
// app's HTMD_MAX_PACKET_SIZE and protocol version so that
// the client could adjust itself for proper communication
#define LLC_APP_HELLO 0 // params: int 'HTMD',
// ^int HTMD_MAX_PACKET_SIZE, int protoVerMajor, int protoVerMinor
// begin outgoing (i. e., from the app) command
#define LLC_APP_CMD_BEGIN 1 // params: int unit_id, int total_cmd_len
// a chunk of outgoing command, none is sent for zero length cmd
#define LLC_APP_CMD_CHUNK 2 // params: int unit_id, data...
// cancel of outgoing command
// (no 'done' llc, it'll be automatically as enough bytes are sent)
#define LLC_APP_CMD_CANCEL 3 // params: int unit_id
// request the client to download a file from the app
#define LLC_APP_FILE_DNLD 4 // params: int size, sugg_name/filter...
// request the client to upload a file into the app
#define LLC_APP_FILE_UPLD 5	// params: sugg_name/filter...
// next chunk of downloaded file sent to the client
#define LLC_APP_FILE_CHUNK 6 // params: data...
// cancel of downloading or uploading file by app's initiative
#define LLC_APP_FILE_CANCEL 7 // params: none
// command to open window for a unit
#define LLC_APP_WINDOW_OPEN 8 // params: int unit_id, url...
// command to close window for a unit
#define LLC_APP_WINDOW_CLOSE 9 // params: int unit_id

// client side (here):
// say hello, an obsolete command that does nothing, can also
// be possibly used for heartbeat ping
#define LLC_CLT_HELLO 0 // params: none
// begin incoming (i. e, into the app) command
#define LLC_CLT_CMD_BEGIN 1 // params: int unit_id, int total_cmd_len
// a chunk of incoming command, none is sent for zero length cmd
#define LLC_CLT_CMD_CHUNK 2  // params: int unit_id, data...
// cancel of incoming command
// (no 'done' llc, it'll be automatically as enough bytes are received)
#define LLC_CLT_CMD_CANCEL 3 // params: int unit_id
// confirmation to the client of statring downloading a file
#define LLC_CLT_ACK_FILE_DNLD 4 // params: user_name...
// confirmation to the client of statring uploading a file
#define LLC_CLT_ACK_FILE_UPLD 5 // params: int size,user_name...
// next chunk of uploaded file sent to the app
#define LLC_CLT_FILE_CHUNK 6 // params: data...
// cancel of downloading or uploading file by client's initiative
#define LLC_CLT_FILE_CANCEL 7 // params: none
// unit ready (invoked htmdUnit.turnIn), the client is not guaranteed
// to handle app commands until this notification
#define LLC_CLT_UNIT_TURN_IN 8 // params: int unit_id
// unit suspended (user closed unit window, reloaded or navigated
// elsewhere), the client is not guaranteed to handle app commands
// after this event and until LLC_CLT_UNIT_TURN_IN
#define LLC_CLT_UNIT_SUSPEND 9 // params: int unit_id

HtMdConnection::HtMdConnection (QObject *parent):
	QObject (parent),
	mState (S_DISCONNECTED),
	mPacketMaxSize (MIN_MAX_PACKET_SIZE), // min value by default
	mInPacketWriting (false),
	mInFileOperation (false),
	mOutFilePacketsPending (0)
{
	assert (spInstance==NULL);
	spInstance = this;

	QObject::connect(&mSocket,SIGNAL(connected()),
		this,SLOT(socketConnected ()));
	QObject::connect(&mSocket,SIGNAL(bytesWritten(qint64)),
		this,SLOT(bytesWritten(qint64)));
	QObject::connect(&mSocket,SIGNAL(readyRead()),
		this,SLOT(bytesRead()));

	QObject::connect(&APP,SIGNAL(fileConfirmed(QString,bool)),
		this,SLOT(fileConfirmed(QString,bool)));
	QObject::connect(&APP,SIGNAL(fileCancelled()),
		this,SLOT(fileCancelled()));

	mTimer.setSingleShot(false);
	mTimer.setInterval(20);
	QObject::connect(&mTimer,SIGNAL(timeout()),
		this,SLOT(timerTick()));
}

HtMdConnection::~HtMdConnection ()
{
	assert (mState==S_DISCONNECTED);
	spInstance = NULL;
}

void HtMdConnection::connect ()
{
	assert (spInstance!=NULL);
	HtMdConnection &self = *spInstance;

	if (self.mState==S_CONNECTED)
	{
		// already connected, what's the problem
		emit self.connected ();
	}
	else if (self.mState==S_DISCONNECTED)
	{
		// only if disconnected can we attempt the
		// new connection...

		// parse parameters
		QStringList hostAddr = APP.connectParameters.split (":");
		if (hostAddr.size ()!=2)
		{
			self.mConnectionError = tr("Invalid connection parameters format. Expected: hostaddr:port");
			emit self.disconnected ();
			return;
		}
		bool portOk;
		int portNum = hostAddr[1].toInt (&portOk);
		if (!portOk || portNum<0 || portNum>65535)
		{
			self.mConnectionError = tr("Invalid port number");
			emit self.disconnected ();
			return;
		}
		self.mState = S_CONNECTING;
		self.mConnectionError = "";
		QObject::connect(&self.mSocket,SIGNAL(error(QAbstractSocket::SocketError)),
			&self,SLOT(socketError(QAbstractSocket::SocketError)));
		QObject::connect(&self.mSocket,SIGNAL(disconnected()),
			&self,SLOT(handleConnectionFailed ()));
		self.mSocket.connectToHost(QHostAddress (hostAddr[0]),portNum);
	}
}

void HtMdConnection::handleConnectionFailed ()
{
	// clenaup signals assignment
	QObject::disconnect(&mSocket,SIGNAL(error(QAbstractSocket::SocketError)),
		this,SLOT(socketError(QAbstractSocket::SocketError)));
	QObject::disconnect(&mSocket,SIGNAL(disconnected()),
		this,SLOT(handleConnectionFailed ()));

	mTimer.stop();
	mFile.close ();
	mInFileOperation = false;
	mOutgoingPackets.clear();
	mOutPacketSofar = -1;
	mInPacketSize = -1;
	mOutFilePacketsPending = 0;
	emit disconnected ();
}

void HtMdConnection::disconnect ()
{
	assert (spInstance!=NULL);
	HtMdConnection &self = *spInstance;

	if (self.mState==S_CONNECTED || self.mState==S_CONNECTING)
	{
		// only if we are connected this makes sense...
		self.mConnectionError = "";
		self.mSocket.disconnectFromHost ();
		self.mFile.close ();
	}
	else if (self.mState==S_DISCONNECTED)
	{
		// already, but should emit the signal for uniformity
		self.mConnectionError = "";
		emit self.disconnected ();
	}
}

QString HtMdConnection::getDisconnectionReason ()
{
	assert (spInstance!=NULL);
	HtMdConnection &self = *spInstance;

	return self.mConnectionError;
}

void HtMdConnection::socketConnected ()
{
	mState = S_CONNECTED;

	//mSocket.setSocketOption(QAbstractSocket::LowDelayOption,1);
	mHelloReceived = false;
	mInPacketSize = mOutPacketSofar = -1;
	mOutFilePacketsPending = 0;
	// only emit connected after valid LLC_APP_HELLO reply
	QObject::connect(&mSocket,SIGNAL(disconnected()),
		this,SLOT(socketDisconnected()));
	mTimer.start();
}

void HtMdConnection::socketDisconnected ()
{
	qDebug()<<"Socket disconnect";
	mState = S_DISCONNECTED;
	QObject::disconnect(&mSocket,SIGNAL(error(QAbstractSocket::SocketError)),
		this,SLOT(socketError(QAbstractSocket::SocketError)));
	QObject::disconnect(&mSocket,SIGNAL(disconnected()),
		this,SLOT(socketDisconnected()));

	mOutgoingPackets.clear();
	mOutPacketSofar = -1;
	mInPacketSize = -1;
	mOutFilePacketsPending = 0;
	emit disconnected ();
}

void HtMdConnection::socketError (QAbstractSocket::SocketError error)
{
	qDebug()<<"Socket error:"<<error;
	mState = S_DISCONNECTED;
	QObject::disconnect(&mSocket,SIGNAL(error(QAbstractSocket::SocketError)),
		this,SLOT(socketError(QAbstractSocket::SocketError)));
	QObject::disconnect(&mSocket,SIGNAL(disconnected()),
		this,SLOT(socketDisconnected()));

	mSocket.abort ();
	mConnectionError = tr("Connection error: ")+
		QString ().setNum((int)error);

	handleConnectionFailed ();
}

void HtMdConnection::bytesRead ()
{
RETRY:
	if (mInPacketSize==-1)
	{
		// awaiting packet size (4 bytes)
		if ((int)mSocket.bytesAvailable ()>=4)
		{
			// got it: prepare to read packet
			QByteArray packetSize = mSocket.read (4);
			mInPacketSize =
			(unsigned ((unsigned char)packetSize[0])<<24)+
			(unsigned ((unsigned char)packetSize[1])<<16)+
			(unsigned ((unsigned char)packetSize[2])<<8)+
			(unsigned ((unsigned char)packetSize[3]));
			mInPacket.clear ();
			if (mInPacketSize>mPacketMaxSize)
			{
				mConnectionError = tr("HtMd protocol fault: incoming packet size out of range");
				mSocket.abort ();
				return;
			}
			goto RETRY; // possibly we already have the rest data
		}
	}
	else
	{
		QByteArray packetPart = mSocket.read (mInPacketSize-mInPacket.size ());
		mInPacket.append (packetPart);
		if (mInPacket.size ()>=mInPacketSize)
		{
			// done with this packet, emit it and prepare for next
			mInPacketSize = -1;
			packetReceived (mInPacket);
			goto RETRY; // possibly try next one
		}
	}
}

void HtMdConnection::outPacketDispatched ()
{
	AuxPacket outPacket = mOutgoingPackets.front ();
	mOutgoingPackets.pop_front();
	// drop it immediately due to re-enterance issues

	if (outPacket.isLenPacket)
		return; // length pseudo-packet, ignore

	// packet post-action (for file related ones)
	if (outPacket.size()>=4)
	{
		switch (decodeUint(outPacket,0))
		{
		case LLC_CLT_FILE_CHUNK:
			mOutFilePacketsPending--;
			mFileSofar += outPacket.size()-4;
			APP.setFileProgress (mFileSofar,mFileTotal);
			if (mFileSofar>=mFileTotal)
			{
				// whole file is uploaded, finalize
				mFile.close();
				mInFileOperation = false;
				APP.evtFdmCompleted.trigger ();
			}
			break;
		case LLC_CLT_ACK_FILE_DNLD:
		case LLC_CLT_ACK_FILE_UPLD:
		case LLC_CLT_FILE_CANCEL:
			mFileSofar = 0;
			break;
		}
	}
}

void HtMdConnection::bytesWritten (qint64 /*bytes*/)
{
	tryBeginWriteNextPacket ();
}

void HtMdConnection::tryBeginWriteNextPacket ()
{
	if (mInPacketWriting) return; // anti-reenterance
	mInPacketWriting = true;
	for (;;)
	{
		if (mOutPacketSofar==-1 && mOutgoingPackets.size()<=0)
			// nothing more to sent
			break;
		if (mOutPacketSofar>=0)
		{
			// a packet is in progress
			assert (mOutgoingPackets.size()>0);
			AuxPacket curPacket = mOutgoingPackets.front();
			mOutPacketSofar += mSocket.write
				(curPacket.mid(mOutPacketSofar));
			if (mOutPacketSofar>=curPacket.size())
			{
				// a packet is completed
				outPacketDispatched ();
				mOutPacketSofar = -1;
			}
			else
				break;
		}
		else
		{
			// no packet is not in progress - begin next
			if (mOutgoingPackets.size()>0)
				mOutPacketSofar = 0;
		}
	}
	mInPacketWriting = false;
}

void HtMdConnection::postPacket (QByteArray packet)
{
	if (!mSocket.isOpen()) return;
	unsigned packetLen = packet.size ();
	QByteArray packetLenPkt (4,0);
	packetLenPkt[0] = (packetLen>>24)&0xFF;
	packetLenPkt[1] = (packetLen>>16)&0xFF;
	packetLenPkt[2] = (packetLen>>8)&0xFF;
	packetLenPkt[3] = (packetLen)&0xFF;
	mOutgoingPackets.push_back (AuxPacket (packetLenPkt,true));
	if (packetLen>0)
		mOutgoingPackets.push_back (packet);
	if (packet.size()>4 &&
		decodeUint(packet,0)==LLC_CLT_FILE_CHUNK)
		mOutFilePacketsPending++;
	tryBeginWriteNextPacket ();
}

void HtMdConnection::timerTick ()
{
	// we'll need to backup readyRead by a timer
	// because fuckiNagel algorithm or whatever
	// often holds out the new data notification
	// for indefinitely long until there will be
	// a certain number of bytes. TCP_NODELAY/
	// QAbstractSocket::LowDelayOption on both app
	// and client sides do not help.
	bytesRead ();

	tryBeginWriteNextPacket (); // also needed time to time

	// TODO: in file uploading mode, pick next things from file
	if (mFile.isOpen() && mFileUploading && !mFile.atEnd())
	{
		while (mOutFilePacketsPending*mPacketMaxSize<MAX_UPLD_FILE_PENDING_BYTES)
		{
			QByteArray fileChunk = serializeUint(LLC_CLT_FILE_CHUNK);
			fileChunk += mFile.read (mPacketMaxSize-4);
			if (fileChunk.size()>4)
				postPacket (fileChunk);
			else
				break; // looks like eof
		}
	}
}

//
// commands handling
//

unsigned HtMdConnection::decodeUint (const QByteArray &ba,int idx)
{
	return (unsigned ((unsigned char)ba[idx])<<24)+
		(unsigned ((unsigned char)ba[idx+1])<<16)+
		(unsigned ((unsigned char)ba[idx+2])<<8)+
		(unsigned ((unsigned char)ba[idx+3]));
}

void HtMdConnection::serializeUint (QByteArray &ba,int idx,unsigned theUint)
{
	ba[idx] = (theUint>>24)&0xFF;
	ba[idx+1] = (theUint>>16)&0xFF;
	ba[idx+2] = (theUint>>8)&0xFF;
	ba[idx+3] = (theUint)&0xFF;
}

QByteArray HtMdConnection::serializeUint (unsigned theUint)
{
	QByteArray ba (4,0);
	ba[0] = (theUint>>24)&0xFF;
	ba[1] = (theUint>>16)&0xFF;
	ba[2] = (theUint>>8)&0xFF;
	ba[3] = (theUint)&0xFF;
	return ba;
}

void HtMdConnection::packetReceived (QByteArray packet)
{
	// incoming (from app) command
	if (packet.size()<4)
	{
		mConnectionError = tr("HtMd protocol fault: packet len <4 bytes, at least cmd code expected");
		mSocket.abort ();
		return;
	}

	unsigned cmdCode = decodeUint (packet,0);
	/*
	qDebug ("Incoming command: %d bytes, %d",packet.size(),cmdCode);
	QString bytes;
	for (int u=0; u<packet.size (); u++)
	{
		char thing[32];
		sprintf (thing,"%02X",int ((unsigned char)packet[u]));
		bytes.append (thing);
	}
	qDebug ()<<bytes;
	*/

	if (!mHelloReceived)
	{
		if (packet.size()<24)
		{
			mConnectionError = tr("HtMd protocol fault: first expected server cmd is LLC_APP_HELLO (received <24 bytes)");
			mSocket.abort ();
			return;
		}
		if (cmdCode!=LLC_APP_HELLO)
		{
			mConnectionError = tr("HtMd protocol fault: first expected server cmd is LLC_APP_HELLO (cmd code is not LLC_AP_HELLO)");
			mSocket.abort ();
			return;
		}
		if (packet[4]!='H' ||
			packet[5]!='T' ||
			packet[6]!='M' ||
			packet[7]!='D')
		{
			mConnectionError = tr("HtMd protocol fault: first expected server cmd is LLC_APP_HELLO (no 'HTMD' signature)");
			mSocket.abort ();
			return;
		}
		unsigned packetSize = decodeUint (packet,8),
			verMajor = decodeUint (packet,12),
			verMinor = decodeUint(packet,16);
		if (verMajor!=HTMD_PROTO_VER_MAJOR || verMinor>HTMD_PROTO_VER_MINOR)
		{
			mConnectionError = tr("HtMd protocol fault: incompatible protocol version");
			mSocket.abort ();
			return;
		}
		if (packetSize>MAX_MAX_PACKET_SIZE)
		{
			mConnectionError = tr("HtMd protocol fault: offered packet size beyond client's capabilities");
			mSocket.abort ();
			return;
		}
		// checks passed, session data received, connect established
		mPacketMaxSize = packetSize;
		mHelloReceived = true;
	}
	else
	{
		Unit *pUnit;
		unsigned unitId;
		int bytes;
		switch (cmdCode)
		{
		case LLC_APP_WINDOW_OPEN:
			if (packet.size()<8)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_WINDOW_OPEN (<8 bytes)");
				mSocket.abort ();
				return;
			}
			unitId = decodeUint (packet,4);
			pUnit = APP.getOrCreateUnit (unitId);
			pUnit->loadURL(QUrl (packet.mid(8)));
			break;
		case LLC_APP_WINDOW_CLOSE:
			if (packet.size()<8)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_WINDOW_OPEN (<8 bytes)");
				mSocket.abort ();
				return;
			}
			unitId = decodeUint (packet,4);
			APP.closeUnit (unitId);
			break;
		case LLC_APP_CMD_BEGIN:
			if (packet.size()<12)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_CMD_BEGIN (<12 bytes)");
				mSocket.abort ();
				return;
			}
			unitId = decodeUint (packet,4);
			pUnit = APP.getUnit (unitId);
			if (pUnit!=NULL)
			{
				pUnit->mCmdFromAppSofar = 0;
				pUnit->mCmdFromAppTotal = decodeUint (packet,8);
				pUnit->mCmdFromApp.clear ();
				pUnit->mCmdFromApp.reserve (pUnit->mCmdFromAppTotal);
				// do not care of possibly unfinished previous command,
				// if LLC_APP_CMD_BEGIN occurs preliminarily it means
				// that previous command is automatically cancelled
				if (pUnit->mCmdFromAppTotal==0)
				{
					// zero length command, execute at once
					pUnit->mCmdFromAppSofar = -1;
					pUnit->mCmdFromAppTotal = -1;
					pUnit->execAppCommand (pUnit->mCmdFromApp);
					pUnit->mCmdFromApp.clear ();
				}
			}
			break;
		case LLC_APP_CMD_CHUNK:
			if (packet.size()<8)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_CMD_CHUNK (<8 bytes)");
				mSocket.abort ();
				return;
			}
			unitId = decodeUint (packet,4);
			pUnit = APP.getUnit (unitId);
			if (pUnit==NULL || pUnit->mCmdFromAppTotal==-1)
				break; // no unit or the command is not expected
			bytes = qMin (
				pUnit->mCmdFromAppTotal-pUnit->mCmdFromAppSofar,
				packet.size()-8);
			pUnit->mCmdFromApp.append(packet.mid(8,bytes));
			if ((pUnit->mCmdFromAppSofar += bytes)>=
				pUnit->mCmdFromAppTotal)
			{
				// command completed, do it
				pUnit->mCmdFromAppSofar = -1;
				pUnit->mCmdFromAppTotal = -1;
				pUnit->execAppCommand (pUnit->mCmdFromApp);
				pUnit->mCmdFromApp.clear ();
			}
			break;
		case LLC_APP_CMD_CANCEL:
			if (packet.size()<8)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_CMD_CANCEL (<8 bytes)");
				mSocket.abort ();
				return;
			}
			unitId = decodeUint (packet,4);
			pUnit = APP.getUnit (unitId);
			if (pUnit==NULL || pUnit->mCmdFromAppTotal==-1)
				break; // no unit or the command is not expected
			// silently cancel the command
			pUnit->mCmdFromAppSofar = -1;
			pUnit->mCmdFromAppTotal = -1;
			pUnit->mCmdFromApp.clear ();
			break;

		case LLC_APP_FILE_DNLD:
			if (packet.size()<8)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_FILE_DNLD (<8 bytes)");
				mSocket.abort ();
				return;
			}
			// size of the file reported from the app
			mFileTotal = decodeUint (packet,4);
			mInFileOperation = true;
			{
				QString name,filter,nameFilter = QString::fromUtf8
					(packet.data()+8,packet.size()-8);
				int pos = nameFilter.indexOf(';');
				if (pos!=-1)
				{
					name = nameFilter.mid (0,pos);
					filter = nameFilter.mid (pos+1);
				}
				else
					name = filter = nameFilter;
				APP.fileSuggestedFilter = filter;
				APP.fileSuggestedName = name;
				APP.evtFdmDownloadRequest.trigger ();
			}

			break;

		case LLC_APP_FILE_UPLD:
			if (packet.size()<4)
			{
				mConnectionError = tr("HtMd protocol fault: invalid LLC_APP_FILE_UPLD (<8 bytes)");
				mSocket.abort ();
				return;
			}
			mInFileOperation = true;
			APP.fileSuggestedFilter = QString::fromUtf8
				(packet.data()+4,packet.size()-4);
			APP.evtFdmUploadRequest.trigger ();
			break;

		case LLC_APP_FILE_CHUNK:
			if (mFile.isOpen () && !mFileUploading)
			{
				QByteArray chunk = packet.mid (4);
				int bytesThisTime = mFileTotal-mFileSofar;
				if (chunk.size ()<bytesThisTime)
					bytesThisTime = chunk.size ();
				if ((int)mFile.write(chunk)<bytesThisTime)
				{
					// error! (at least it looks so and can't be
					// treated other way)
					// TODO: error notification alert
					APP.evtFdmCancel.trigger ();
				}
				mFileSofar += bytesThisTime;
				APP.setFileProgress (mFileSofar,mFileTotal);
				if (mFileSofar>=mFileTotal)
				{
					mFile.close ();
					mInFileOperation = false;
					APP.evtFdmCompleted.trigger ();
				}
			}
			else
				// unexpected chunk, possibly desync, tell the app
				// that it makes a mistake
				APP.evtFdmCancel.trigger ();
			break;

		case LLC_APP_FILE_CANCEL:
			APP.evtFdmCancel.trigger ();
			break;
		}
	}
}

// services

void HtMdConnection::postClientCommandBegin
	(unsigned unitId,int totalLen)
{
	QByteArray pkt;
	pkt.reserve(8+totalLen);
	pkt += serializeUint(LLC_CLT_CMD_BEGIN);
	pkt += serializeUint(unitId);
	pkt += serializeUint(totalLen);
	postPacket (pkt);
}

void HtMdConnection::postClientCommandChunk
	(unsigned unitId,QByteArray chunk)
{
	QByteArray pkt;
	pkt.reserve(8+chunk.size());
	pkt += serializeUint(LLC_CLT_CMD_CHUNK);
	pkt += serializeUint(unitId);
	pkt += chunk;
	postPacket (pkt);
}

void HtMdConnection::postClientCommandCancel
	(unsigned unitId)
{
	QByteArray pkt;
	pkt.reserve(8);
	pkt += serializeUint(LLC_CLT_CMD_CANCEL);
	pkt += serializeUint(unitId);
	postPacket (pkt);
}

void HtMdConnection::postClientUnitTurnIn
	(unsigned unitId)
{
	QByteArray pkt;
	pkt.reserve(8);
	pkt += serializeUint(LLC_CLT_UNIT_TURN_IN);
	pkt += serializeUint(unitId);
	postPacket (pkt);
}

void HtMdConnection::postClientUnitSuspend
	(unsigned unitId)
{
	QByteArray pkt;
	pkt.reserve(8);
	pkt += serializeUint(LLC_CLT_UNIT_SUSPEND);
	pkt += serializeUint(unitId);
	postPacket (pkt);
}

void HtMdConnection::postClientFileCancel ()
{
	QByteArray pkt;
	pkt.reserve(4);
	pkt += serializeUint(LLC_CLT_FILE_CANCEL);
	postPacket (pkt);
}

void HtMdConnection::postClientFileDownloadConfirm
	(QString actualName)
{
	QFileInfo qfi (actualName);
	QByteArray pkt;
	pkt.reserve(4);
	pkt += serializeUint(LLC_CLT_ACK_FILE_DNLD);
	pkt += qfi.fileName().toUtf8 ();
	pkt.truncate (mPacketMaxSize);
	postPacket (pkt);
}

void HtMdConnection::postClientFileUploadConfirm
	(QString actualName,int size)
{
	QFileInfo qfi (actualName);
	QByteArray pkt;
	pkt.reserve(8);
	pkt += serializeUint(LLC_CLT_ACK_FILE_UPLD);
	pkt += serializeUint(size);
	pkt += qfi.fileName().toUtf8 ();
	pkt.truncate (mPacketMaxSize);
	postPacket (pkt);
}

bool HtMdConnection::isConnected ()
{
	return mState==S_CONNECTED;
}

// handlers

void HtMdConnection::fileConfirmed (QString fileName,bool upload)
{
	mFile.close ();
	mFile.setFileName (fileName);
	if (upload)
	{
		mFile.open (QFile::ReadOnly);
		mFileSofar = 0;
		mFileTotal = mFile.size ();
	}
	else
	{
		mFile.open (QFile::WriteOnly);
		// mFileTotal is filled by incoming command
		mFileSofar = 0;
	}
	mFileUploading = upload;
	if (!mFile.isOpen ())
	{
		// TODO: error notification alert
		APP.evtFdmCancel.trigger ();
	}
	else
	{
		if (upload)
			postClientFileUploadConfirm (fileName,mFileTotal);
		else
			postClientFileDownloadConfirm (fileName);
	}

	// check for zero len files case
	if (mFileTotal<=0)
	{
		// zero len file, no chunks to send/expect, finish it now
		mFile.close ();
		mInFileOperation = false;
		APP.evtFdmCancel.trigger ();
		return;
	}
}

void HtMdConnection::fileCancelled ()
{
	if (mInFileOperation)
	{
		mFile.close ();
		postClientFileCancel ();
		mInFileOperation = false;
	}
}
